package org.zn.note.jdk.net;

import org.zn.note.common.util.IOUtil;
import org.zn.note.common.util.ThreadUtil;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * @ClassName JavaNio
 * @Author zn
 * @Date 2019/4/22 8:42
 **/
/*
BIO byte streams、character streams
NIO Jdk1.4、new IO、non-blocking IO、Channels、Buffers、Selectors
    read：channel --> buffer
    write：buffer --> channel
    Selectors：monitor multiple channels for events (like: connection opened, data arrived etc.).
                a single thread can monitor multiple channels for data.

Channel vs Stream
    ① Channel 双向的，可读写；Stream 单向的，比如InputStream、OutputStream
    ② Channel 异步读写（asynchronously）
    ③ Channel 无论读写，都是用Buffer（from/to Buffer）
    FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel

Buffer
    capacity 容量、position 位置、limit 界线
    （写模式）当往Buffer中写时（可能是自己 buf.put，也可能是 channel.read(buf)）
        position 表示在该位置写入，每写一个单位 position + 1，最大到 capacity - 1
        limit 表示最多能写多少数据，此时 limit = capacity
    （写模式 切换成 读模式） --> buf.flip()
        limit = position（limit 记录最后写到的位置），position = 0
    （读模式）当从Buffer中读时
        position 表示从该位置读取，每读一个单位 position + 1，最多读取到 limit
     总之，无论读写：
        ① if position >= limit  throw new BufferOverflowException();
        ② position ++
        ③ 在 position位置读写
     其实Buffer的所有操作，都是基于 capacity、position、limit 操作的封装，比如：
     flip()、rewind()、clear()、compact() 等
 */
public class JavaNio {

    private static final String FILE_PATH_A = "d:/test/a.txt";
    private static final String FILE_PATH_B = "d:/test/b.txt";
    private static final String TEST_STRING = "0123456789";
    private static final String MOD_READ_WRITE = "rw";

    private static void printFile(String filePath) {
        System.out.println("----> printFile :");
        RandomAccessFile file = null;
        try {
            file = new RandomAccessFile(filePath, MOD_READ_WRITE);
            // Buffer 的构造方法不是public的，所以不能自己 new
            FileChannel inChannel = file.getChannel();
            ByteBuffer buf = ByteBuffer.allocate(48);
            int bytesRead = inChannel.read(buf);
            while (bytesRead != -1) {
//                System.out.println("Read " + bytesRead);
                buf.flip();
                while (buf.hasRemaining()) {
                    System.out.print((char) buf.get());
                }
                buf.clear();
                bytesRead = inChannel.read(buf);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IOUtil.close(file);
        }
        System.out.print("\n");
    }

    private static void testByteBuffer() {
        System.out.println("----> testByteBuffer :");
        RandomAccessFile file = null;
        try {
            file = new RandomAccessFile(FILE_PATH_A, MOD_READ_WRITE);
            FileChannel channel = file.getChannel();
            // Buffer 的构造方法不是public的，所以不能自己 new，要用这种方式
            ByteBuffer buf = ByteBuffer.allocate(7);
            // Buffer 是一个内存块，放入数据后，通过position可以随意操作
            int bytesRead = channel.read(buf);
            System.out.println("read size:" + bytesRead);
            // 指定获取Buffer中第2个元素，这样可以
            System.out.println((char) buf.get(1));
            // position = 0
            buf.flip();
            // 如果不flip，这里会报错，因为 position是7，get是先挪动指针再取，但是已经是最后一个位置了
            System.out.println((char) buf.get());
            // 在第2个位置(此时position=1)，写入z，覆盖了以前的'b'
            buf.put((byte) 'z');
            System.out.println((char) buf.get());
            // 读出'z'
            System.out.println((char) buf.get(1));
            System.out.println((char) buf.get());
            channel.write(buf);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IOUtil.close(file);
        }
        System.out.print("\n");
    }

    public static void writeFile(String filePath, String content) {
        System.out.println("----> writeFile :");
        File f = new File(filePath);
        RandomAccessFile file = null;
        try {
            // 原有文件不删的话，RandomAccessFile 是按照 position位置 和 内容长度 覆盖文件原有内容的，并保留未覆盖内容
            // 文件内容0123456789；position = 1，content = abc；修改后 0abc456789
            f.deleteOnExit();
            f.createNewFile();
            file = new RandomAccessFile(f, MOD_READ_WRITE);
            FileChannel channel = file.getChannel();
            byte[] bContent = content.getBytes();
            ByteBuffer buf = ByteBuffer.allocate(bContent.length);
            buf.put(bContent);
            buf.flip();
            channel.write(buf);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IOUtil.close(file);
        }
        System.out.print("\n");
    }

    private static void transferFrom(String src, String target) throws IOException {
        System.out.println("----> transferFrom :");
        // 关注用法，没有 try、catch、finally、close
        RandomAccessFile fromFile = new RandomAccessFile(src, MOD_READ_WRITE);
        RandomAccessFile toFile = new RandomAccessFile(target, MOD_READ_WRITE);
        // FileChannelImpl
        FileChannel fromChannel = fromFile.getChannel();
        FileChannel toChannel = toFile.getChannel();
        long position = 0;
        long count = fromChannel.size();
        System.out.println("position " + position + ",count " + count);
        // srcChannel.transferTo(long position, long count, WritableByteChannel target)
        // targetChannel.transferFrom(ReadableByteChannel src, long position, long count)
        long transferBytes = toChannel.transferFrom(fromChannel, position, count);
        System.out.println("transferBytes " + transferBytes);
        System.out.print("\n");
    }

    // 默认的ServerSocketChannel是阻塞的，和传统的ServerSocket流程一样
    private static void testBlockedIO() throws IOException {
        int port = 8080;
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(port));
        while (true) {
            // 默认是阻塞式的，到这会卡住
            // 相当于当前线程一直在这监听，有一个连接获取一个socketChannel，然后交由另外一个线程处理
            // 和传统的ServerSocket类似，只不过用的 Channel和Buffer操作的
            ThreadUtil.printThread("wait connection.");
            SocketChannel socketChannel = serverSocketChannel.accept();
            ThreadUtil.printThread("connect from " + socketChannel.getRemoteAddress());
            // 阻塞式的，每个连接都要分配一个线程“阻塞”住，以等待接收
            // 这就是阻塞式和非阻塞式最大的区别！
            new Thread(new Runnable() {
                @Override
                public void run() {
                    int byteBufferCapacity = 1024;
                    ByteBuffer byteBuffer = ByteBuffer.allocate(byteBufferCapacity);
                    try {
                        while (true) {
                            ThreadUtil.printThread("wait read data.");
                            byteBuffer.clear();
                            int readLength = socketChannel.read(byteBuffer);
                            ThreadUtil.printThread("read length:" + readLength + ", read content:" + new String(byteBuffer.array()));
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    } finally {
                        IOUtil.close(socketChannel);
                    }
                }
            }).start();
        }
    }

    // 非阻塞式IO
    private static void testNonBlockedIO() throws IOException {
        int port = 8080;
        int capacity = 1024;
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 设置为非阻塞式的
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(port));
        List<SocketChannel> list = new ArrayList<>();
        ByteBuffer byteBuffer = ByteBuffer.allocate(capacity);
        while (true) {
            // 非阻塞的，立即返回，没有则null
            SocketChannel s = serverSocketChannel.accept();
            if (null != s) {
                list.add(s);
            }
            for (SocketChannel socketChannel : list) {
                byteBuffer.clear();
                // 当没有信息可读时，这里会卡住，也就是说这里肯定是阻塞的
                // serverSocketChannel.configureBlocking(false); 仅仅是 accept不阻塞，但是 read仍然阻塞
                // 本来想模拟 Select 在一个线程里处理 多个连接的 accept和read，看来不行了
                // 可是Select怎么实现的非阻塞监听read状态的呢？
                int readLength = socketChannel.read(byteBuffer);
                if (0 != readLength) {
                    ThreadUtil.printThread("read length:" + readLength + ", read content:" + new String(byteBuffer.array()));
                }
            }
        }
    }

    private static void testNonBlockedIOUsedSelector() throws IOException {
        int port = 8080;
        int capacity = 1024;
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        Selector selector = Selector.open();
        // 注册感兴趣的事件
        // 这块只能OP_ACCEPT，其他的会 IllegalArgumentException
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //  注册到selector的所有channel
        Set<SelectionKey> allKeys = selector.keys();
        // 触发了channel选择的动作的（）
        Set<SelectionKey> selectedKeys = selector.selectedKeys();
        serverSocketChannel.bind(new InetSocketAddress(port));
    }

    private static void printSelectionKeys(Set<SelectionKey> keys) {
        if (0 == keys.size()) {
            ThreadUtil.printThread("not selectionKey.");
        }
        for (SelectionKey selectionKey : keys) {
            ThreadUtil.printThread("selectionKey.isAcceptable() = " + selectionKey.isAcceptable());
            ThreadUtil.printThread("selectionKey.isConnectable() = " + selectionKey.isConnectable());
            ThreadUtil.printThread("selectionKey.isReadable() = " + selectionKey.isReadable());
            ThreadUtil.printThread("selectionKey.isWritable() = " + selectionKey.isWritable());
            ThreadUtil.printThread("selectionKey.isValid() = " + selectionKey.isValid());
        }
    }

    public static void main(String[] args) throws IOException {
        testNonBlockedIOUsedSelector();
    }
}
